/*
 * Copyright 2009-2010 the original author or authors.
 *
 * Licensed under the Apache license, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.internna.iwebjtracker.javamail.server;

import javax.mail.Address;
import javax.mail.Session;
import javax.mail.Message;
import javax.mail.Transport;
import java.util.Properties;
import java.util.Collection;
import javax.mail.Authenticator;
import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import javax.mail.PasswordAuthentication;
import javax.mail.internet.InternetAddress;
import java.io.UnsupportedEncodingException;
import javax.mail.internet.AddressException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.internna.iwebjtracker.project.model.IWebJTrackerUser;
import org.internna.iwebmvc.IWebMvcException;
import org.internna.iwebmvc.utils.CollectionUtils;
import org.internna.iwebjtracker.server.mail.MailServer;
import org.internna.iwebjtracker.javamail.model.MessageQueue;
import org.internna.iwebjtracker.model.UsernamePasswordCredentials;
import org.internna.iwebjtracker.javamail.model.SmtpConnectionDetails;
import org.internna.iwebjtracker.javamail.model.JavamailSessionWrapper;
import org.internna.iwebjtracker.server.AbstractConnectionManagerServer;

import static org.internna.iwebmvc.utils.StringUtils.hasText;
import static org.internna.iwebmvc.utils.StringUtils.validateEmail;

/**
 *
 * @author Jose Noheda
 * @since 1.0
 */
public final class JavamailServer extends AbstractConnectionManagerServer<JavamailSessionWrapper, SmtpConnectionDetails> implements MailServer<JavamailSessionWrapper, SmtpConnectionDetails> {

    private static final Log logger = LogFactory.getLog(JavamailServer.class);

    /**
     * STMP protocol
     */
    public static final String SMTP = "smtp";

    private boolean useSSL;
    private final Session session;
    private final InternetAddress from;
    private final MessageQueue messageQueue;
    private int batchSize = MessageQueue.BATCH_SIZE;
    private final SmtpConnectionDetails connectionDetails;

    /**
     * Configures the server.
     */
    public JavamailServer(final Properties properties, String from, String fromAddress) throws UnsupportedEncodingException {
        super();
        messageQueue = new MessageQueue();
        this.from = new InternetAddress(fromAddress, from);
        connectionDetails = createConnectionDetails(properties);
        useSSL = "true".equals(properties.getProperty("ssl"));
        boolean useTLS = "true".equals(properties.getProperty("tls"));
        session = Session.getInstance(createJavamailEnvironment(useTLS));
        session.setDebug(Boolean.valueOf(properties.getProperty("debug")));
    }

    /**
     * Configures connection details needed to check validity.
     *
     * @param properties a non null instance
     * @return a non null instance
     */
    protected SmtpConnectionDetails createConnectionDetails(Properties properties) {
        SmtpConnectionDetails details = new SmtpConnectionDetails();
        details.setHost(properties.getProperty("host"));
        details.setPort(Integer.parseInt(properties.getProperty("port")));
        if (hasText(properties.getProperty("username"))) {
            UsernamePasswordCredentials credentials = new UsernamePasswordCredentials();
            credentials.setUsername(properties.getProperty("username"));
            credentials.setPassword(properties.getProperty("password"));
            details.setCredentials(credentials);
        }
        return details;
    }

    /**
     * Establish all the Javamail environment properties (like host, port, TLS...)
     *
     * @param useTLS any
     * @param useSSL any
     * @return a non null non empty instance
     */
    protected Properties createJavamailEnvironment(boolean useTLS) {
        Properties data = new Properties();
        String protocol = "mail.smtp.";
        if (useSSL) {
            protocol = "mail.smtps.";
            data.setProperty(protocol + "socketFactory.class", "javax.net.ssl.SSLSocketFactory");
            data.setProperty(protocol + "ssl.protocols", "SSLv3 TLSv1");
        }
        data.setProperty(protocol + "user", from.getAddress());
        data.setProperty("mail.host", connectionDetails.getHost());
        data.setProperty(protocol + "socketFactory.fallback", "false");
        data.setProperty(protocol + "starttls.enable", String.valueOf(useTLS));
        String port = String.valueOf(connectionDetails.getPort());
        data.setProperty(protocol + "port", port);
        data.setProperty(protocol + "socketFactory.port", port);
        data.setProperty(protocol + "auth", String.valueOf(connectionDetails.getCredentials() != null));
        return data;
    }

    /**
     * Creates a SMTP authenticator if needed.
     *
     * @return any
     */
    protected Authenticator createAuthenticator() {
        final UsernamePasswordCredentials creds = connectionDetails.getUsernamePasswordCredentials();
        return creds == null ? null : new Authenticator() {
            @Override protected PasswordAuthentication getPasswordAuthentication() {
                return new PasswordAuthentication(creds.getUsername(), creds.getPassword());
            }
        };
    }

    /**
     * SMTP does not use the protocol part of the url so it always validates.
     *
     * @param protocol any
     * @return true
     */
    @Override protected boolean validateProtocol(String protocol) {
        return true;
    }

    /**
     * Delegates the logic to the connection.
     *
     * @param connection a non null instance
     */
    @Override protected void doCloseConnection(JavamailSessionWrapper connection) {
        connection.close();
    }

    /**
     * Tries to open a physical transport layer.
     *
     * @param connectionDetails a non null instance
     * @return any
     */
    @Override protected JavamailSessionWrapper doOpenConnection(SmtpConnectionDetails connectionDetails) {
        try {
            Transport transport = session.getTransport(useSSL ? "smtps" : "smtp");
            JavamailSessionWrapper connection = new JavamailSessionWrapper();
            connection.setTransport(transport);
            transport.connect(connectionDetails.getUsernamePasswordCredentials().getUsername(), connectionDetails.getUsernamePasswordCredentials().getPassword());
            return connection;
        } catch (Exception ex) {
            if (logger.isInfoEnabled()) {
                logger.info("Could not open connection to mail server: " + ex.getMessage());
            }
            return null;
        }
    }

    /**
     * Actually sends all the pending messages in the queue.
     */
    @Override public void send() throws MessagingException {
        Collection<MimeMessage> toSend = messageQueue.take(batchSize);
        if (CollectionUtils.isNotEmpty(toSend)) {
            if (logger.isDebugEnabled()) {
                logger.debug("Mail server session inititated. Sending [" + toSend.size() + "] mails in current batch.");
            }
            JavamailSessionWrapper connection = openConnection(connectionDetails);
            for (MimeMessage message : toSend) {
                send(connection, message);
            }
            closeConnection(connection);
        }
    }

    private void send(JavamailSessionWrapper connection, MimeMessage message) throws MessagingException {
        if (connection != null) {
            connection.getTransport().sendMessage(message, message.getAllRecipients());
        } else {
            messageQueue.enqueue(message);
        }
    }

    /**
     * Stores the email for later operation (to achieve a non-blocking behavior).
     *
     * @param message any
     */
    @Override public boolean send(MimeMessage message) {
        boolean result = message != null;
        if (result) {
            try {
                message.saveChanges();
                result = messageQueue.enqueue(message);
            } catch (MessagingException ex) {
                result = false;
            }
        }
        return result;
    }

    /**
     * Creates an empty message with a predefined from address established.
     *
     * @return a non null instance
     */
    @Override public MimeMessage createMessage() {
        try {
            MimeMessage message = new MimeMessage(session);
            message.setFrom(from);
            return message;
        } catch (MessagingException me) {
            throw new IWebMvcException(me);
        }
    }

    /**
     * Delegates to createMessage(null, to).
     *
     * @param to any
     * @return a non null instance
     */
    @Override public MimeMessage createMessage(String to) {
        return createMessage(null, to);
    }

    /**
     * Creates an address object if able and delegates to createMessage(address)
     *
     * @param name any
     * @param to any
     * @return a non null instance
     */
    @Override public MimeMessage createMessage(String name, String to) {
        try {
            InternetAddress address = hasText(to) ? new InternetAddress(to, name) : null;
            return createMessage(address);
        } catch (UnsupportedEncodingException ex) {
            throw new IWebMvcException(ex);
        }
    }

    private void addAddress(MimeMessage message, Address address) {
        if (address != null) {
            try {
                message.addRecipient(Message.RecipientType.TO, address);
            } catch (MessagingException ex) {
                throw new IWebMvcException(ex);
            }
        }
    }

    /**
     * Delegates message creation and sets the destination.
     *
     * @param address any
     * @return
     */
    @Override public MimeMessage createMessage(Address address) {
        MimeMessage message = createMessage();
        addAddress(message, address);
        return message;
    }

    /**
     * Delegates message creation and sets multiple destinations.
     *
     * @param users any
     * @return
     */
    @Override public MimeMessage createMessage(Collection<IWebJTrackerUser> users) {
        MimeMessage message = createMessage();
        if (CollectionUtils.isNotEmpty(users)) {
            for (IWebJTrackerUser user : users) {
                addAddress(message, getAddress(user));
            }
        }
        return message;
    }

    protected Address getAddress(IWebJTrackerUser user) {
        Address address = null;
        if ((user != null) && validateEmail(user.getEmail())) {
            try {
                address = new InternetAddress(user.getEmail());
            } catch (AddressException ex) {
                throw new IWebMvcException(ex);
            }
        }
        return address;
    }

    /**
     * Identifies this server as a Javamail server implementation.
     *
     * @return Javamail
     */
    @Override public String getServerType() {
        return "Javamail";
    }

    /**
     * For testing purposes.
     *
     * @return the configured message queue
     */
    protected MessageQueue getMessageQueue() {
        return messageQueue;
    }

}
